package queue;

import java.util.LinkedList;
import java.util.NoSuchElementException;

import page.Page;
import page.PageParser;
import data.DataGatherer;

/**
 * Queue which holds Page objects.  
 * Multi-threaded and thread-safe.
 * 
 * Creates pop-up threads as items are added.
 * 
 * @author Ken "David" McClain
 * @author Peter Mosher
 * @version 1
 */
//To avoid warnings about PageBuffer not being serializable
@SuppressWarnings("serial") 
public class PageBuffer extends LinkedList<Page> implements Runnable {
	
	/** Maximum number of popup threads we are allowed to generate. */
	private final int numberOfThreads;
	
	/** List of page retriever threads. */
	private final PageParser[] pageParsers;
	
	/** Queue to enqueue retrieved pages. */
	private final PageToRetrieve pageToRetrieve;
	
	/** List of words to look for in the page, and count. */
	private final String[] keywords;
	
	/** Data Gatherer to report results to. */
	private final DataGatherer dataGatherer;
	
	/** Flag to stop the thread from generating new pop-up threads. */
	private boolean stop = false;
	
	/** Thread waits on this object when there are no URLs to retrieve. */
	private BlockingObject emptyBlock = new BlockingObject();

	/** Thread waits on this object when all threads are currently running. */
	private BlockingObject threadsFullBlock = new BlockingObject();

	/** Thread waits on this object when all threads are currently running. */
	private BlockingObject threadFinishedBlock;
	
	/**
	 * Constructor.
	 * @param ptr PageToRetrieve queue object that holds URLs to retrieve.
	 * @param threadNum Max number of threads to have open at any time.
	 * @param kws Keywords to look for in each page.
	 * @param dg Data Gatherer object to report to.
	 */
	public PageBuffer(final PageToRetrieve ptr, int threadNum, String[] kws, DataGatherer dg) {
		super();
		pageToRetrieve = ptr;
		
		keywords = kws;
		dataGatherer = dg;
		
		// Initialize PageParser threads.
		numberOfThreads = threadNum;
		pageParsers = new PageParser[numberOfThreads];
		for (int i = 0; i < pageParsers.length; i++) 
			pageParsers[i] = new PageParser(keywords, pageToRetrieve, dataGatherer);
	}
	
	/**
	 * Sets the object to be notified when threads complete.
	 * @param block The object to be notified when threads complete.
	 */
	public void setFinishedBlock(final BlockingObject block) {
		threadFinishedBlock = block;
	}
	
	
	/** 
	 * Thread-safe method to add a page to the queue.
	 * @param page The Page to add.
	 */
	synchronized public boolean add(final Page page) {
		boolean result;
		
		// Add page to queue.
		result = super.add(page);
		
		// Wake up the thread in case it is waiting for a new item in the queue.
		synchronized (emptyBlock) {
			emptyBlock.notify();
		}
		
		return result;
	}
	
	
	/** Thread-safe method to check if queue is empty. */
	synchronized public boolean isEmpty() { return super.isEmpty(); }
	
	
	/**
	 * Thread-safe method to remove the next page from the queue.
	 * @throws NoSuchElementException If the queue is empty.
	 */
	synchronized public Page remove() throws NoSuchElementException {
		return super.remove();
	}
	
	
	/** Tells this thread to stop. */
	public void stop() {
		this.stop = true;
		
		// Wake up the thread in case it was blocking.
		synchronized (emptyBlock) {
			emptyBlock.notify();
		}
		
		synchronized (threadsFullBlock) {
			threadsFullBlock.notify();
		}
		
	}
	
	
	/**
	 * @return True if the queue is empty AND all threads are not 'busy'. False otherwise.
	 */
	synchronized public boolean isStopped() {
		if (!isEmpty())
			return false;
		
		for (PageParser pp : pageParsers) {
			if (pp.isBusy())
				return false;
		}
		
		return true;
	}
	
	
	/**
	 * Threaded loop. Waits for Pages to appear in the queue.
	 * Loops until "stop" is true (the stop() method is invoked).
	 */
	public void run() {
		
		while (!stop && !dataGatherer.hitLimit()) {
			
			/* This is the old method, which utilizes SPIN-LOCKING and is BAD.
			if (isEmpty())
				continue;
			*/
			
			// If queue is empty, we "block".
			if (isEmpty()) {
				// Tells thread to wait until someone calls "notify()" on emptyBlock.
				try {
					synchronized (emptyBlock) {
						emptyBlock.wait();
					}
				} catch (InterruptedException ie) { }
			}
			
			// If we were blocking and are now going to stop, exit the loop.
			if (stop) 
				break;
			
			// At this point, we have something in the queue to grab.
			
			// Find a thread that is not "busy".
			int availableThread = -1; // -1 is a flag to let us know no threads are available
			
			while (availableThread == -1 && !stop) {
				
				for (int i = 0; i < pageParsers.length; i++) {
					if (!pageParsers[i].isBusy()) {
						availableThread = i;
						break;
					}
				}
				
				// If there are no threads available, BLOCK!
				if (availableThread == -1) {
					try {
						synchronized (threadsFullBlock) {
							threadsFullBlock.wait();
							// The code will halt execution here until "threadsFullBlock" is notified
							// Which happens when a thread completes execution.
						}
					} catch (InterruptedException ie) { }
				}
				
			}
			
			if (!stop) {
				pageParsers[availableThread] = new PageParser(keywords, pageToRetrieve, dataGatherer);
				pageParsers[availableThread].setBusy(true);
				pageParsers[availableThread].setPage(remove());
				pageParsers[availableThread].setBlockingObject(threadsFullBlock);
				pageParsers[availableThread].setBlockedUntilFinished(threadFinishedBlock);
				pageParsers[availableThread].start();
			}
			
		}
		
		// Join with other threads, wait until they finish.
		for (PageParser pp : pageParsers) {
			try {
				pp.join();
			} catch (InterruptedException ie) { }
		}
		
	}
}
